Skip to content

bugfix(particlesys): Delay creation of particle emitters until ParticleSystemManager is xfer-loaded#2333

Open
stephanmeesters wants to merge 7 commits intoTheSuperHackers:mainfrom
stephanmeesters:bugfix-nonsavable-emitters-xfer
Open

bugfix(particlesys): Delay creation of particle emitters until ParticleSystemManager is xfer-loaded#2333
stephanmeesters wants to merge 7 commits intoTheSuperHackers:mainfrom
stephanmeesters:bugfix-nonsavable-emitters-xfer

Conversation

@stephanmeesters
Copy link
Copy Markdown

@stephanmeesters stephanmeesters commented Feb 20, 2026

During loading a savegame, some particle systems were created before ParticleSystemManager had the opportunity to xfer-load the saved particle systems, which led to the possibility that some of these early created particle systems could be overwritten in m_systemMap of ParticleSystemManager. When that happens they would no longer reliably be able to use master/slave systems. The retail game does not use master/slave on the involved effects but it could appear as an issue with some mods (as the bug report describes too).

Misc findings

W3DTankDraw and W3DTankTruckDraw would create particle systems in the constructor, only to purge and recreate them again in postProcess, this has been changed to look if we're loading a savegame and then only create it once.

GrantStealthBehavior triggers when you do a GPS scrambler, but the behavior and associated particle effect will only last for one frame. The particle effect references a texture that's available in Generals but not in Zero Hour. So I think that so far nobody has been able to see this particle effect in action.

Todo

  • Replicate in Generals

@stephanmeesters stephanmeesters force-pushed the bugfix-nonsavable-emitters-xfer branch from 7418b31 to 16135b2 Compare February 22, 2026 19:54
@stephanmeesters stephanmeesters marked this pull request as ready for review February 22, 2026 20:43
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 22, 2026

Greptile Summary

This PR fixes a save-game race condition where particle systems were instantiated inside object constructors before ParticleSystemManager::xfer() had restored its own state from the save file, potentially allowing early-created systems to overwrite restored ones in m_systemMap and break master/slave relationships.

The fix is applied consistently across five call sites:

  • W3DTankDraw / W3DTankTruckDraw: Emitter creation moved from the constructor to the first doDrawModule() call (lazy via INVALID_PARTICLE_SYSTEM_ID guard), and tossTreadEmitters() is no longer called unconditionally during loadPostProcess() since there is nothing to toss.
  • AutoHealBehavior / GrantStealthBehavior: Creation extracted to a new createEmitters() helper that is called from update() (first live frame) and loadPostProcess() (save-game restore path); the INVALID_PARTICLE_SYSTEM_ID guard prevents double-creation on either path.
  • ParticleSys.cpp: A DEBUG_ASSERTCRASH is added to the xfer-load path to make future regressions immediately visible.

Confidence Score: 5/5

Safe to merge — the fix is correct and consistent; remaining findings are minor style/maintenance suggestions only.

All P1 concerns from previous review rounds (double-creation guards, indentation) have been addressed. The two remaining findings — a duplicated static helper and a lost static_assert — are P2 style suggestions that do not affect correctness or runtime behaviour.

W3DTankDraw.cpp and W3DTankTruckDraw.cpp contain the duplicated static helper and the unrolled loop that dropped the static_assert.

Important Files Changed

Filename Overview
Core/GameEngine/Source/GameClient/System/ParticleSys.cpp Adds a DEBUG_ASSERTCRASH to the xfer-load path to catch particle systems created before the manager has a chance to restore its state — the central guard that motivates the whole PR.
Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DTankDraw.cpp Moves tread-emitter creation from constructor to first draw call; refactors common logic into a new static helper, but the helper is duplicated in W3DTankTruckDraw.cpp and the original static_assert safety check was removed during loop unrolling.
Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DTankTruckDraw.cpp Same structural changes as W3DTankDraw.cpp: deferred tread-emitter creation and identical static helper; the same duplication and lost static_assert issues apply.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/AutoHealBehavior.cpp Particle system creation extracted to createEmitters(), guarded by INVALID_PARTICLE_SYSTEM_ID check and called lazily from update() and loadPostProcess(); correctly addresses the save-load race described in the PR.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/GrantStealthBehavior.cpp Same deferred-creation pattern as AutoHealBehavior; createEmitters() is properly guarded and called from both update() and loadPostProcess().
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/AutoHealBehavior.h Adds createEmitters() private method declaration; minor cosmetic brace-style change on the class definition.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/GrantStealthBehavior.h Adds createEmitters() private method declaration alongside m_radiusParticleSystemID.

Sequence Diagram

sequenceDiagram
    participant L as SaveGame Loader
    participant PSM as ParticleSystemManager
    participant OBJ as GameObject (e.g. Tank)
    participant DRAW as W3DTankDraw
    participant BEH as AutoHealBehavior

    Note over L,BEH: Save-game load sequence (fixed)

    L->>OBJ: construct object
    OBJ->>DRAW: constructor (no emitters created)
    OBJ->>BEH: constructor (no emitters created)

    L->>PSM: xfer() — restores particle systems from save
    PSM-->>L: m_systemMap fully populated

    L->>DRAW: loadPostProcess()
    DRAW->>PSM: createTreadEmitters() [safe, PSM ready]
    L->>BEH: loadPostProcess()
    BEH->>PSM: createEmitters() [safe, PSM ready]

    Note over DRAW,PSM: During normal gameplay (non-save)
    DRAW->>PSM: createTreadEmitters() on first doDrawModule()
    BEH->>PSM: createEmitters() on first update()
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DTankDraw.cpp
Line: 128-142

Comment:
**Duplicate static helper duplicated across translation units**

The `createParticleSystem` static helper function is defined identically in both `W3DTankDraw.cpp` (lines 128–142) and `W3DTankTruckDraw.cpp` (lines 184–198). Any future bug fix or behavioural change needs to be applied in two places. Consider extracting it to a shared header or a common utility source file (e.g. a `W3DParticleUtils` inline/static function) that both TUs include.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DTankDraw.cpp
Line: 144-157

Comment:
**`static_assert` array-size check removed by loop unrolling**

The original loop used a `static_assert` to guarantee that `treadDebrisNames` and `m_treadDebrisIDs` have the same number of elements. Unrolling the loop into two separate `if`-blocks silently dropped this compile-time safety net. If `MAX_TREAD_IDS` (or the underlying array) is ever extended to three or more entries, the third slot will never be populated and the mismatch will go undetected.

The same `static_assert` pattern is still preserved in `W3DTankTruckDraw::createWheelEmitters()`, so restoring it here would be consistent:

```cpp
void W3DTankDraw::createTreadEmitters()
{
    if (getW3DTankDrawModuleData())
    {
        static_assert(ARRAY_SIZE(m_treadDebrisIDs) == 2, "Update createTreadEmitters if array size changes");
        if (m_treadDebrisIDs[0] == INVALID_PARTICLE_SYSTEM_ID)
            m_treadDebrisIDs[0] = createParticleSystem(getW3DTankDrawModuleData()->m_treadDebrisNameLeft, getDrawable());
        if (m_treadDebrisIDs[1] == INVALID_PARTICLE_SYSTEM_ID)
            m_treadDebrisIDs[1] = createParticleSystem(getW3DTankDrawModuleData()->m_treadDebrisNameRight, getDrawable());
    }
}
```

The same applies to `W3DTankTruckDraw::createTreadEmitters()` at the equivalent location.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (7): Last reviewed commit: "Add assert in ParticleSystemManager" | Re-trigger Greptile

@stephanmeesters stephanmeesters force-pushed the bugfix-nonsavable-emitters-xfer branch from 16135b2 to 7b4b2fd Compare February 22, 2026 20:48
@stephanmeesters stephanmeesters changed the title bugfix(particlesys): Delay creation of non-savable particle emitters until postProcess bugfix(particlesys): Delay creation of particle emitters until postProcess Feb 22, 2026
Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

createTreadEmitters();
// TheSuperHackers @bugfix stephanmeesters 20/02/2026
// If loading from savegame, delay non-saveable emitter creation until postProcess.
if (TheGameState == nullptr || TheGameState->isInLoadGame() == FALSE)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is calling a function that is commented with "brutal hack", indicating that using this function is undesired. Can we solve this another way perhaps? Perhaps create particle effects later?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Particle effects are now created later, in update or doDrawModule. When loading from savegame these happen after loadPostProcess.

I've reworked the create emitters functions a bit to be more performant and readable

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my first thought here is that this is a workaround for a limitation of the particle system manager; changing the particle creation on object creation to a lazy update creation. This is similar to what the wheel emitters do. However, it does not fix the root issue with the particle system manager. So if someone created particles on object creation in the future again, then the issue returns. Is there a way to make the particle system manager robust against this? Or at least add an assert that signals that it is invalid?

Copy link
Copy Markdown
Author

@stephanmeesters stephanmeesters Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an assert in ParticleSystemManager::xfer that tests that the list is empty on load, this seems like a good thing to have.

One thing we can do is move CHUNK_ParticleSystem before CHUNK_GameLogic, which means the particle system manager is xfer-loaded early. I verified that this will fix the order of adding particle systems. This only applies to new saves as old saves load the chunks in the order it was saved.

However adding particle systems in the constructor will be problematic regardless of chunk order in the case of a savegame: in the old code there was tossTreadEmitters() in postProcess(), which appears to be needed because otherwise these early created particles are no longer visible for some reason.

So doing the lazy initalization seems like a good thing. Moving the chunks around would hide this particular problem (and may cause other issues idk).

@stephanmeesters stephanmeesters marked this pull request as draft February 28, 2026 11:32
@stephanmeesters stephanmeesters changed the title bugfix(particlesys): Delay creation of particle emitters until postProcess bugfix(particlesys): Delay creation of particle emitters until ParticleSystemManager is loaded Mar 14, 2026
@stephanmeesters stephanmeesters changed the title bugfix(particlesys): Delay creation of particle emitters until ParticleSystemManager is loaded bugfix(particlesys): Delay creation of particle emitters until ParticleSystemManager is xfer-loaded Mar 14, 2026
stephanmeesters and others added 5 commits March 15, 2026 19:20
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
…toHealBehavior.cpp

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@stephanmeesters stephanmeesters force-pushed the bugfix-nonsavable-emitters-xfer branch from ee59fc0 to 1f55acd Compare March 15, 2026 18:50
@stephanmeesters stephanmeesters marked this pull request as ready for review March 15, 2026 19:06
@xezon xezon added Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Saveload Is Saveload/Xfer related labels Mar 15, 2026
@xezon xezon self-requested a review March 15, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something is not working right, typically is user facing Gen Relates to Generals Minor Severity: Minor < Major < Critical < Blocker Saveload Is Saveload/Xfer related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W3DTankDraw::createEmitters create new particle system before XFER_LOAD of TheParticleSystemManager cause save crash..

2 participants